/**
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.guicerecipes.support;

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

import org.guicerecipes.*;
import org.guicerecipes.support.internal.*;
import org.guicerecipes.util.*;

import com.google.common.collect.*;
import com.google.inject.*;
import com.google.inject.binder.*;
import com.google.inject.matcher.*;
import com.google.inject.name.*;
import com.google.inject.spi.*;

import static com.google.inject.matcher.Matchers.*;
import static org.guicerecipes.support.EncounterProvider.*;

/**
 * Adds some new helper methods to the base Guice module
 * 
 * @version $Revision: 1.1 $
 */
@SuppressWarnings("unchecked")
public abstract class GuiceyFruitModule extends AbstractModule {

	@Override
	protected void configure() {
		// lets find all of the configures methods
		List<Method> configureMethods = getConfiguresMethods();
		if (!configureMethods.isEmpty()) {
			final GuiceyFruitModule moduleInstance = this;
			final Class<? extends GuiceyFruitModule> moduleType = getClass();
			TypeLiteral<? extends GuiceyFruitModule> type = TypeLiteral.get(moduleType);

			for (final Method method : configureMethods) {
				int size = method.getParameterTypes().length;
				if (size == 0) {
					throw new ProvisionException("No arguments on @Configures method " + method);
				} else if (size > 1) {
					throw new ProvisionException("Too many arguments " + size + " on @Configures method " + method);
				}
				final Class<?> paramType = getParameterType(type, method, 0);

				bindListener(new AbstractMatcher<TypeLiteral<?>>() {
					public boolean matches(TypeLiteral<?> typeLiteral) {
						return typeLiteral.getRawType().equals(paramType);
					}
				}, new TypeListener() {
					public <I> void hear(TypeLiteral<I> injectableType, TypeEncounter<I> encounter) {
						encounter.register(new MembersInjector<I>() {
							public void injectMembers(I injectee) {
								// lets invoke the configures method
								try {
									method.setAccessible(true);
									method.invoke(moduleInstance, injectee);
								} catch (IllegalAccessException e) {
									throw new ProvisionException("Failed to invoke @Configures method " + method + ". Reason: " + e, e);
								} catch (InvocationTargetException ie) {
									Throwable e = ie.getTargetException();
									throw new ProvisionException("Failed to invoke @Configures method " + method + ". Reason: " + e, e);
								}
							}
						});
					}
				});
			}
		}
	}

	private List<Method> getConfiguresMethods() {
		List<Method> answer = Lists.newArrayList();
		List<Method> list = Reflectors.getAllMethods(getClass());
		for (Method method : list) {
			if (method.getAnnotation(Configures.class) != null) {
				answer.add(method);
			}
		}
		return answer;
	}

	/**
	 * Binds a post injection hook method annotated with the given annotation to the given method handler.
	 */
	protected <A extends Annotation> void bindMethodHandler(final Class<A> annotationType, final MethodHandler methodHandler) {
		bindMethodHandler(annotationType, encounterProvider(methodHandler), false);
	}

	protected <A extends Annotation> void bindMethodHandler(final Class<A> annotationType, final MethodHandler methodHandler, boolean lookInSuperClass) {
		bindMethodHandler(annotationType, encounterProvider(methodHandler), lookInSuperClass);
	}

	/**
	 * Binds a post injection hook method annotated with the given annotation to the given method handler.
	 */
	protected <A extends Annotation> void bindMethodHandler(final Class<A> annotationType, final Key<? extends MethodHandler> methodHandlerKey) {
		bindMethodHandler(annotationType, encounterProvider(methodHandlerKey), false);
	}

	/**
	 * Binds a post injection hook method annotated with the given annotation to the given method handler.
	 */
	protected <A extends Annotation> void bindMethodHandler(final Class<A> annotationType, final Class<? extends MethodHandler> methodHandlerType) {
		bindMethodHandler(annotationType, encounterProvider(methodHandlerType), false);
	}

	private <A extends Annotation> void bindMethodHandler(final Class<A> annotationType, final EncounterProvider<MethodHandler> encounterProvider, final boolean lookInSuperClass) {

		bindListener(any(), new TypeListener() {
			public <I> void hear(TypeLiteral<I> injectableType, TypeEncounter<I> encounter) {
				Class<? super I> type = injectableType.getRawType();
				final Method method = Reflection.findMethodWithAnnotation(type, annotationType, lookInSuperClass);
				if (method != null) {
					final A annotation = method.getAnnotation(annotationType);
					final Provider<? extends MethodHandler> provider = encounterProvider.get(encounter);

					encounter.register(new InjectionListener<I>() {
						public void afterInjection(I injectee) {

							MethodHandler methodHandler = provider.get();
							try {
								methodHandler.afterInjection(injectee, annotation, method);
							} catch (InvocationTargetException ie) {
								Throwable e = ie.getTargetException();
								throw new ProvisionException(e.getMessage(), e);
							} catch (IllegalAccessException e) {
								throw new ProvisionException(e.getMessage(), e);
							}
						}
					});
				}
			}
		});
	}

	/**
	 * Binds a custom injection point for a given injection annotation to the annotation member provider so that occurrences of the annotation on fields and methods with a single parameter will be
	 * injected by Guice after the constructor and @Inject have been processed.
	 * 
	 * @param annotationType the annotation class used to define the injection point
	 * @param annotationMemberProviderKey the key of the annotation member provider which can be instantiated and injected by guice
	 * @param <A> the annotation type used as the injection point
	 */
	protected <A extends Annotation> void bindAnnotationInjector(Class<A> annotationType, Key<? extends AnnotationMemberProvider> annotationMemberProviderKey) {

		bindAnnotationInjector(annotationType, encounterProvider(annotationMemberProviderKey));
	}

	/**
	 * Binds a custom injection point for a given injection annotation to the annotation member provider so that occurrences of the annotation on fields and methods with a single parameter will be
	 * injected by Guice after the constructor and @Inject have been processed.
	 * 
	 * @param annotationType the annotation class used to define the injection point
	 * @param annotationMemberProvider the annotation member provider which can be instantiated and injected by guice
	 * @param <A> the annotation type used as the injection point
	 */
	protected <A extends Annotation> void bindAnnotationInjector(Class<A> annotationType, AnnotationMemberProvider annotationMemberProvider) {

		bindAnnotationInjector(annotationType, encounterProvider(annotationMemberProvider));
	}

	/**
	 * Binds a custom injection point for a given injection annotation to the annotation member provider so that occurrences of the annotation on fields and methods with a single parameter will be
	 * injected by Guice after the constructor and @Inject have been processed.
	 * 
	 * @param annotationType the annotation class used to define the injection point
	 * @param annotationMemberProviderType the type of the annotation member provider which can be instantiated and injected by guice
	 * @param <A> the annotation type used as the injection point
	 */
	protected <A extends Annotation> void bindAnnotationInjector(Class<A> annotationType, Class<? extends AnnotationMemberProvider> annotationMemberProviderType) {

		bindAnnotationInjector(annotationType, encounterProvider(annotationMemberProviderType));
	}

	private <A extends Annotation> void bindAnnotationInjector(final Class<A> annotationType, final EncounterProvider<AnnotationMemberProvider> memberProviderProvider) {

		bindListener(any(), new TypeListener() {
			Provider<? extends AnnotationMemberProvider> providerProvider;

			public <I> void hear(TypeLiteral<I> injectableType, TypeEncounter<I> encounter) {

				Set<Field> boundFields = Sets.newHashSet();
				Map<MethodKey, Method> boundMethods = Maps.newHashMap();

				TypeLiteral<?> startType = injectableType;
				while (true) {
					Class<?> type = startType.getRawType();
					if (type == Object.class) {
						break;
					}

					Field[] fields = type.getDeclaredFields();
					for (Field field : fields) {
						if (boundFields.add(field)) {
							bindAnnotationInjectorToField(encounter, startType, field);
						}
					}

					Method[] methods = type.getDeclaredMethods();
					for (final Method method : methods) {
						MethodKey key = new MethodKey(method);
						if (boundMethods.get(key) == null) {
							boundMethods.put(key, method);
							bindAnnotationInjectionToMember(encounter, startType, method);
						}
					}

					Class<?> supertype = type.getSuperclass();
					if (supertype == Object.class) {
						break;
					}
					startType = startType.getSupertype(supertype);
				}
			}

			protected <I> void bindAnnotationInjectionToMember(final TypeEncounter<I> encounter, final TypeLiteral<?> type, final Method method) {
				// TODO lets exclude methods with @Inject?
				final A annotation = method.getAnnotation(annotationType);
				if (annotation != null) {
					if (providerProvider == null) {
						providerProvider = memberProviderProvider.get(encounter);
					}

					encounter.register(new MembersInjector<I>() {
						public void injectMembers(I injectee) {
							AnnotationMemberProvider provider = providerProvider.get();

							int size = method.getParameterTypes().length;
							Object[] values = new Object[size];
							for (int i = 0; i < size; i++) {
								Class<?> paramType = getParameterType(type, method, i);
								Object value = provider.provide(annotation, type, method, paramType, i);
								checkInjectedValueType(value, paramType, encounter);

								// if we have a null value then assume the injection point cannot be satisfied
								// which is the spring @Autowired way of doing things
								if ((value == null) && !provider.isNullParameterAllowed(annotation, method, paramType, i)) {
									return;
								}
								values[i] = value;
							}
							try {
								method.setAccessible(true);
								method.invoke(injectee, values);
							} catch (IllegalAccessException e) {
								throw new ProvisionException("Failed to inject method " + method + ". Reason: " + e, e);
							} catch (InvocationTargetException ie) {
								Throwable e = ie.getTargetException();
								throw new ProvisionException("Failed to inject method " + method + ". Reason: " + e, e);
							}
						}
					});
				}
			}

			protected <I> void bindAnnotationInjectorToField(final TypeEncounter<I> encounter, final TypeLiteral<?> type, final Field field) {
				// TODO lets exclude fields with @Inject?
				final A annotation = field.getAnnotation(annotationType);
				if (annotation != null) {
					if (providerProvider == null) {
						providerProvider = memberProviderProvider.get(encounter);
					}

					encounter.register(new InjectionListener<I>() {
						public void afterInjection(I injectee) {
							AnnotationMemberProvider provider = providerProvider.get();
							Object value = provider.provide(annotation, type, field);
							checkInjectedValueType(value, field.getType(), encounter);

							try {
								field.setAccessible(true);
								field.set(injectee, value);
							} catch (IllegalAccessException e) {
								throw new ProvisionException("Failed to inject field " + field + ". Reason: " + e, e);
							}
						}
					});
				}
			}
		});
	}

	protected Class<?> getParameterType(TypeLiteral<?> type, Method method, int i) {
		Class<?>[] parameterTypes = method.getParameterTypes();
		List<TypeLiteral<?>> list = type.getParameterTypes(method);
		TypeLiteral<?> typeLiteral = list.get(i);

		Class<?> paramType = typeLiteral.getRawType();
		if ((paramType == Object.class) || (paramType.isArray() && (paramType.getComponentType() == Object.class))) {
			// if the TypeLiteral ninja doesn't work, lets fall back to the actual type
			paramType = parameterTypes[i];
		}
		return paramType;
	}

	/*
	 * protected void bindCloseHook() { bindListener(any(), new Listener() { public <I> void hear(InjectableType<I> injectableType, Encounter<I> encounter) { encounter.registerPostInjectListener(new
	 * InjectionListener<I>() { public void afterInjection(I injectee) {
	 * 
	 * } }); } }); }
	 */

	/**
	 * Returns true if the value to be injected is of the correct type otherwise an error is raised on the encounter and false is returned
	 */
	protected <I> void checkInjectedValueType(Object value, Class<?> type, TypeEncounter<I> encounter) {
		// TODO check the type
	}

	/**
	 * A helper method to bind the given type with the binding annotation.
	 * 
	 * This allows you to replace this code <code> bind(Key.get(MyType.class, SomeAnnotation.class))
   * </code>
	 * 
	 * with this <code> bind(KMyType.class, SomeAnnotation.class) </code>
	 */
	protected <T> LinkedBindingBuilder<T> bind(Class<T> type, Class<? extends Annotation> annotationType) {
		return bind(Key.get(type, annotationType));
	}

	/**
	 * A helper method to bind the given type with the binding annotation.
	 * 
	 * This allows you to replace this code <code> bind(Key.get(MyType.class, someAnnotation))
   * </code>
	 * 
	 * with this <code> bind(KMyType.class, someAnnotation) </code>
	 */
	protected <T> LinkedBindingBuilder<T> bind(Class<T> type, Annotation annotation) {
		return bind(Key.get(type, annotation));
	}

	/**
	 * A helper method to bind the given type with the {@link com.google.inject.name.Named} annotation of the given text value.
	 * 
	 * This allows you to replace this code <code> bind(Key.get(MyType.class, Names.named("myName")))
   * </code>
	 * 
	 * with this <code> bind(KMyType.class, "myName") </code>
	 */
	protected <T> LinkedBindingBuilder<T> bind(Class<T> type, String namedText) {
		return bind(type, Names.named(namedText));
	}

	/**
	 * A helper method which binds a named instance to a key defined by the given name and the instances type. So this method is short hand for
	 * 
	 * <code> bind(instance.getClass(), name).toInstance(instance); </code>
	 */
	protected <T> void bindInstance(String name, T instance) {
		// TODO not sure the generics ninja to avoid this cast
		Class<T> aClass = (Class<T>) instance.getClass();
		bind(aClass, name).toInstance(instance);
	}
}
